package work.mediway.mdm.biz.interceptor;

import com.alibaba.druid.sql.SQLUtils;
import com.alibaba.druid.sql.ast.SQLExpr;
import com.alibaba.druid.sql.ast.SQLStatement;
import com.alibaba.druid.sql.ast.expr.SQLBinaryOpExpr;
import com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlDeleteStatement;
import com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlInsertStatement;
import com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlUpdateStatement;
import com.alibaba.druid.sql.dialect.mysql.parser.MySqlStatementParser;
import com.alibaba.druid.sql.dialect.mysql.visitor.MySqlSchemaStatVisitor;
import com.alibaba.druid.stat.TableStat;
import com.alibaba.druid.util.JdbcConstants;
import com.baomidou.mybatisplus.core.MybatisParameterHandler;
import com.mediway.hos.app.base.seure.util.SecurityUtils;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.sql.Statement;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import lombok.extern.slf4j.Slf4j;
import work.mediway.mdm.core.constant.MdmConstant;
import work.mediway.mdm.core.enums.StatusEditEnum;
import work.mediway.mdm.core.mapper.CommonMapper;
import work.mediway.mdm.core.service.DataChangeLogService;

/**
 * @author zhongyj <1126834403@qq.com><br/>
 * @date 2021/7/12
 */
@Slf4j
@Order(value = Ordered.HIGHEST_PRECEDENCE)
@Intercepts({
        @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
        @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})
})
public class SqlLogInterceptor implements Interceptor {

    private Configuration configuration = null;

    private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT_THREAD_LOCAL = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"));

    @Override
    public Object intercept(Invocation invocation) throws InvocationTargetException, IllegalAccessException {
        Object target = invocation.getTarget();

        long startTime = System.currentTimeMillis();
        List<Map<String, Object>> oldData = CollUtil.newArrayList();
        String tableCode = "";
        StatusEditEnum operaType = null;
        // 是否需要记录日志
        boolean logFlag = false;

        // 获取SQL、解析
        String sql = this.getSql(target);

        MySqlStatementParser parser = new MySqlStatementParser(SQLUtils.format(sql, JdbcConstants.MYSQL));
        SQLStatement sqlStatement = parser.parseStatement();
        MySqlSchemaStatVisitor visitor = new MySqlSchemaStatVisitor();
        sqlStatement.accept(visitor);
        Map<TableStat.Name, TableStat> tableStatMap = visitor.getTables();

        // 如果表名数量为0或者大于1，不支持
        if (tableStatMap.size() != 1) {
            log.info("SQL解析表名为空或者为多表关联SQL, tableCount : {}, 暂未支持", tableStatMap.size());
            return invocation.proceed();
        }

        for (Map.Entry<TableStat.Name, TableStat> tableStatEntry : tableStatMap.entrySet()) {
            tableCode = StrUtil.removePrefixIgnoreCase(tableStatEntry.getKey().getName(), MdmConstant.SCHEMA_NAME_);
            operaType = StatusEditEnum.get(StrUtil.toString(tableStatEntry.getValue()).toUpperCase());
        }

        for (String exclude : MdmConstant.MDM_LOG) {
            if (StrUtil.contains(tableCode, exclude)) {
                logFlag = true;
                break;
            }
        }
        if (logFlag) {
            // 记录修改之前数据
            if (StatusEditEnum.STATUS_INSERT_FULL != operaType) {
                SQLExpr where = null;
                if (StatusEditEnum.STATUS_DELETE_FULL == operaType) {
                    MySqlDeleteStatement deleteStatement = (MySqlDeleteStatement) sqlStatement;
                    where = deleteStatement.getWhere();
                } else if (StatusEditEnum.STATUS_UPDATE_FULL == operaType) {
                    MySqlUpdateStatement updateStatement = (MySqlUpdateStatement) sqlStatement;
                    where = updateStatement.getWhere();
                }
                if (ObjectUtil.isNotEmpty(where)) {
                    // 查询数据
                    String oldDataSql = "SELECT * FROM " + tableCode + " WHERE " + where;
                    log.info("查询数据变更之前的SQL: {}", oldDataSql);
                    List<Map<String, Object>> items = SpringUtil.getBean(CommonMapper.class).getPublicItems(oldDataSql);
                    log.info("查询结果为: {}, {}", items.size(), items);
                    oldData.addAll(items);
                }
            }
        }

        if (logFlag) {
            // 注册事务同步处理,如果有事务确保目标方法所在事务提交之后再执行后置操作
            if (TransactionSynchronizationManager.isSynchronizationActive()) {
                String finalTableCode = tableCode;
                StatusEditEnum finalOperaType = operaType;
                TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
                    @Override
                    public void afterCommit() {
                        //执行后置操作
                        dealLogAndPush(oldData, finalTableCode, finalOperaType, sqlStatement);
                    }
                });
            } else {
                dealLogAndPush(oldData, tableCode, operaType, sqlStatement);
            }
            log.info(StrUtil.format("操作数据的执行SQL为: {}    执行耗时={}ms ", sql, System.currentTimeMillis() - startTime));
        }

        return invocation.proceed();
    }

    private void dealLogAndPush(List<Map<String, Object>> oldData, String tableCode, StatusEditEnum operaType, SQLStatement sqlStatement) {
        // 获取表名和操作类型
        List<Map<String, Object>> newData = CollUtil.newArrayList();
        Map<String, Object> newRecord = MapUtil.newHashMap();
        List<String> newColumns = CollUtil.newArrayList();
        List<String> newValues = CollUtil.newArrayList();

        if (operaType == StatusEditEnum.STATUS_INSERT_FULL) {
            assert sqlStatement instanceof MySqlInsertStatement;
            MySqlInsertStatement insertStatement = (MySqlInsertStatement) sqlStatement;

            insertStatement.getColumns().forEach(item -> newColumns.add(StrUtil.removeAll(item.toString(), "'")));
            insertStatement.getValuesList().forEach(item -> newValues.add(StrUtil.removeAll(item.toString(), "'")));
        } else if (operaType == StatusEditEnum.STATUS_UPDATE_FULL) {
            assert sqlStatement instanceof MySqlUpdateStatement;
            MySqlUpdateStatement updateStatement = (MySqlUpdateStatement) sqlStatement;

            updateStatement.getItems().forEach(item -> {
                newColumns.add(StrUtil.removeAll(item.getColumn().toString(), "'"));
                newValues.add(StrUtil.removeAll(item.getValue().toString(), "'"));
            });
            SQLBinaryOpExpr where = (SQLBinaryOpExpr) updateStatement.getWhere();
            newColumns.add(StrUtil.removeAll(where.getLeft().toString(), "'"));
            newValues.add(StrUtil.removeAll(where.getRight().toString(), "'"));
        }
        for (int i = 0; i < newColumns.size(); i++) {
            newRecord.put(StrUtil.removeAll(newColumns.get(i), "`"), newValues.get(i));
        }
        newData.add(newRecord);

        // 存储日志
        SpringUtil.getBean(DataChangeLogService.class).saveDictDetailLog(tableCode, oldData, newData, SecurityUtils.getLoginName(), operaType);
    }

    /**
     * 获取sql
     *
     * @param target Object
     * @return String
     */
    private String getSql(Object target) {
        try {
            StatementHandler statementHandler = (StatementHandler) target;
            BoundSql boundSql = statementHandler.getBoundSql();
            if (configuration == null) {
                final MybatisParameterHandler parameterHandler = (MybatisParameterHandler) statementHandler.getParameterHandler();
                Field configurationField = ReflectionUtils.findField(parameterHandler.getClass(), "configuration");

                assert configurationField != null;
                ReflectionUtils.makeAccessible(configurationField);
                this.configuration = (Configuration) configurationField.get(parameterHandler);
            }
            //替换参数格式化Sql语句，去除换行符
            return formatSql(boundSql, configuration);
        } catch (Exception e) {
            log.warn("get sql error {}", target, e);
        }
        return "";
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }

    /**
     * 获取完整的sql实体的信息
     *
     * @param boundSql BoundSql
     * @return String
     */
    private String formatSql(BoundSql boundSql, Configuration configuration) {
        String sql = boundSql.getSql();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        Object parameterObject = boundSql.getParameterObject();
        // 输入sql字符串空判断
        if (sql == null || sql.length() == 0) {
            return "";
        }

        if (configuration == null) {
            return "";
        }

        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();

        // 美化sql
        sql = beautifySql(sql);
        // @see org.apache.ibatis.scripting.defaults.DefaultParameterHandler 参考Mybatis 参数处理
        if (parameterMappings != null) {
            for (ParameterMapping parameterMapping : parameterMappings) {
                if (parameterMapping.getMode() != ParameterMode.OUT) {
                    Object value;
                    String propertyName = parameterMapping.getProperty();
                    if (boundSql.hasAdditionalParameter(propertyName)) {
                        value = boundSql.getAdditionalParameter(propertyName);
                    } else if (parameterObject == null) {
                        value = null;
                    } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                        value = parameterObject;
                    } else {
                        MetaObject metaObject = configuration.newMetaObject(parameterObject);
                        value = metaObject.getValue(propertyName);
                    }
                    String paramValueStr;
                    if (value instanceof String) {
                        paramValueStr = "'" + value + "'";
                    } else if (value instanceof Date) {
                        paramValueStr = "'" + DATE_FORMAT_THREAD_LOCAL.get().format(value) + "'";
                    } else {
                        paramValueStr = value + "";
                    }
                    // java.lang.IllegalArgumentException: Illegal group reference
                    // https://github.com/WangJi92/mybatis-sql-log/issues/4
                    sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(paramValueStr));
                }
            }
        }
        return sql;
    }

    /**
     * 美化Sql
     */
    private String beautifySql(String sql) {
        sql = sql.replaceAll("[\\s\n ]+", " ");
        return sql;
    }
}
